// Written in the D Programming Language
/**
 * The 18th lesson in the <a href="http://nehe.gamedev.net/data/lessons/lesson.asp?lesson=18">NeHe tutorial series</a>.
 * Originally written by GB Schmick.
 *
 * Authors: GB Schmick
 *          Jeff Molofee
 *          Olli Aalto
 */
module lesson18;

import derelict.opengl.gl;
import derelict.opengl.glu;
import derelict.sdl.sdl;

import tango.stdc.stringz;

/// The window title
const char[] WINDOW_TITLE = "NeHe & TipTup's Quadratics Tutorial (D version)";

/// The main loop flag
bool running = true;

/// Lighting ON / OFF
bool light;

bool pageUp;
bool pageDown;
bool up;
bool down;
bool right;
bool left;
bool sp;

/// Start Of Disc	( NEW )
int part1;
/// End Of Disc		( NEW )
int part2;
/// Increase 1		( NEW )
int p1 = 0;
/// Increase 2		( NEW )
int p2 = 1;

/// X Rotation
GLfloat xrot = 0.0f;
/// Y Rotation
GLfloat yrot = 0.0f;
/// X Rotation Speed
GLfloat xspeed = 0.0f;
/// Y Rotation Speed
GLfloat yspeed = 0.0f;
/// Depth Into The Screen
GLfloat z = -5.0f;

/// Ambient Light Values
GLfloat lightAmbient[] = [0.5f, 0.5f, 0.5f, 1.0f];
/// Diffuse Light Values
GLfloat lightDiffuse[] = [1.0f, 1.0f, 1.0f, 1.0f];
/// Light Position
GLfloat lightPosition[] = [0.0f, 0.0f, 2.0f, 1.0f];

/// Which Filter To Use
GLuint filter;
/// Storage for 3 textures
GLuint textures[3];

/// Storage For Our Quadratic Objects ( NEW )
GLUquadric* quadratic;

/// Which Object To Draw (NEW)
GLuint obj;

/**
 * Module constructor. Here we load the GL, GLU and SDL shared libraries,
 * and the initialize SDL.
 */
static this()
{
	DerelictGL.load();
	DerelictGLU.load();
	DerelictSDL.load();

	if (SDL_Init(SDL_INIT_EVERYTHING) < 0)
	{
		throw new Exception("Failed to initialize SDL: " ~ getSDLError());
	}
}

/**
 * Module destructor. SDL_Quit must be called somewhere, and as we initialized
 * it in the module constructor so the module destructor should be a suitable
 * place.
 */
static ~this()
{
	SDL_Quit();
}

/**
 * The main function. This is where the fun begins. The first order of business
 * is the check the command line arguments if the user wanted to start in
 * fullscreen mode. Then the window is created and OpenGL is initialized with
 * basic settings. Finally the the function starts the main loop which will live
 * for the duration of the application.
 *
 * Params:
 *      args = the command line arguments
 */
void main(char[][] args)
{
	bool fullScreen = false;
	if (args.length > 1)
	{
		fullScreen = args[1] == "-fullscreen";
	}

	createGLWindow(WINDOW_TITLE, 640, 480, 16, fullScreen);

	initGL();

	while (running)
	{
		processEvents();

		drawGLScene();

		SDL_GL_SwapBuffers();
		SDL_Delay(10);
	}

	// Delete The Quadratic To Free System Resources
	gluDeleteQuadric(quadratic);
}

/**
 * Function to load in bitmap as a GL texture.
 */
void loadGLTextures()
{
	SDL_Surface* textureImage;

	// Load The Bitmap, Check For Errors, If Bitmap's Not Found Quit
	if ((textureImage = SDL_LoadBMP("data/Wall.bmp")) !is null)
	{
		// Free the surface when we exit the scope
		scope(exit)
			SDL_FreeSurface(textureImage);

		// Create The Textures
		glGenTextures(3, &textures[0]);

		// Create Nearest Filtered Texture
		glBindTexture(GL_TEXTURE_2D, textures[0]);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
		glTexImage2D(GL_TEXTURE_2D, 0, 3, textureImage.w, textureImage.h, 0,
			GL_BGR, GL_UNSIGNED_BYTE, textureImage.pixels);

		// Create Linear Filtered Texture
		glBindTexture(GL_TEXTURE_2D, textures[1]);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
		glTexImage2D(GL_TEXTURE_2D, 0, 3, textureImage.w, textureImage.h, 0,
			GL_BGR, GL_UNSIGNED_BYTE, textureImage.pixels);

		// Create MipMapped Texture
		glBindTexture(GL_TEXTURE_2D, textures[2]);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
			GL_LINEAR_MIPMAP_NEAREST);
		gluBuild2DMipmaps(GL_TEXTURE_2D, 3, textureImage.w, textureImage.h,
			GL_BGR, GL_UNSIGNED_BYTE, textureImage.pixels);

		return;
	}
	throw new Exception("Failed to load texture.");
}

/**
 * Process all the pending events.
 */
void processEvents()
{
	SDL_Event event;
	while (SDL_PollEvent(&event))
	{
		switch (event.type)
		{
			case SDL_KEYUP:
				keyReleased(event.key.keysym.sym);
				break;
			case SDL_QUIT:
				running = false;
				break;
			case SDL_KEYDOWN:
				keyPressed(event.key.keysym.sym);
				break;
			default:
				break;
		}
	}

	if (pageUp)
	// Is Page Up Being Pressed?
	{
		z -= 0.02f; // If So, Move Into The Screen
	}

	if (pageDown)
	// Is Page Down Being Pressed?
	{
		z += 0.02f; // If So, Move Towards The Viewer
	}

	if (up)
	// Is Up Arrow Being Pressed?
	{
		xspeed -= 0.01f; // If So, Decrease xspeed
	}

	if (down)
	// Is Down Arrow Being Pressed?
	{
		xspeed += 0.01f; // If So, Increase xspeed
	}

	if (right)
	// Is Right Arrow Being Pressed?
	{
		yspeed += 0.01f; // If So, Increase yspeed
	}

	if (left)
	// Is Left Arrow Being Pressed?
	{
		yspeed -= 0.01f; // If So, Decrease yspeed
	}

}

void keyPressed(int key)
{
	switch (key)
	{
		case SDLK_PAGEUP:
			pageUp = true;
			break;
		case SDLK_PAGEDOWN:
			pageDown = true;
			break;
		case SDLK_UP:
			up = true;
			break;
		case SDLK_DOWN:
			down = true;
			break;
		case SDLK_RIGHT:
			right = true;
			break;
		case SDLK_LEFT:
			left = true;
			break;
		default:
			break;
	}
}

/**
 * Process a key released event.
 */
void keyReleased(int key)
{
	switch (key)
	{
		case SDLK_ESCAPE:
			running = false;
			break;
		case SDLK_l:
			light = !light;
			if (!light)
			{
				glDisable(GL_LIGHTING);
			}
			else
			{
				glEnable(GL_LIGHTING);
			}
			break;
		case SDLK_f:
			filter++;
			if (filter > 2)
			{
				filter = 0;
			}
			break;
		case SDLK_PAGEUP:
			pageUp = false;
			break;
		case SDLK_PAGEDOWN:
			pageDown = false;
			break;
		case SDLK_UP:
			up = false;
			break;
		case SDLK_DOWN:
			down = false;
			break;
		case SDLK_RIGHT:
			right = false;
			break;
		case SDLK_LEFT:
			left = false;
			break;
		case SDLK_SPACE:
			obj++;
			if (obj > 5)
			{
				obj = 0;
			}
			break;
		default:
			break;
	}
}

/**
 * Resize And Initialize The GL Window.
 */
void resizeGLScene(GLsizei width, GLsizei height)
{
	if (height == 0)
	{
		height = 1;
	}
	// Reset The Current Viewport
	glViewport(0, 0, width, height);

	// Select The Projection Matrix
	glMatrixMode(GL_PROJECTION);
	// Reset The Projection Matrix
	glLoadIdentity();

	// Calculate The Aspect Ratio Of The Window
	gluPerspective(45.0f, cast(GLfloat) width / cast(GLfloat) height, 0.1f,
		100.0f);

	// Select The Modelview Matrix
	glMatrixMode(GL_MODELVIEW);
	// Reset The Modelview Matrix
	glLoadIdentity();
}

/**
 * All Setup For OpenGL Goes Here.
 */
void initGL()
{
	loadGLTextures();
	// Enable Texture Mapping
	glEnable(GL_TEXTURE_2D);
	// Enables Smooth Shading
	glShadeModel(GL_SMOOTH);
	// Black Background
	glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
	// Depth Buffer Setup
	glClearDepth(1.0f);
	// Enables Depth Testing
	glEnable(GL_DEPTH_TEST);
	// The Type Of Depth Test To Do
	glDepthFunc(GL_LEQUAL);
	// Really Nice Perspective Calculations
	glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);

	// Setup The Ambient Light
	glLightfv(GL_LIGHT1, GL_AMBIENT, lightAmbient.ptr);
	// Setup The Diffuse Light
	glLightfv(GL_LIGHT1, GL_DIFFUSE, lightDiffuse.ptr);
	// Position The Light
	glLightfv(GL_LIGHT1, GL_POSITION, lightPosition.ptr);
	// Enable Light One
	glEnable(GL_LIGHT1);

	// Change to texture matrix and flip and rotate the texture
	glMatrixMode(GL_TEXTURE);
	glRotatef(180.0f, 0.0f, 0.0f, 1.0f);
	glScalef(-1.0f, 1.0f, 1.0f);
	// Back to normal
	glMatrixMode(GL_MODELVIEW);

	// Create A Pointer To The Quadric Object ( NEW )
	quadratic = gluNewQuadric();
	// Create Smooth Normals ( NEW )
	gluQuadricNormals(quadratic, GLU_SMOOTH);
	// Create Texture Coords ( NEW )
	gluQuadricTexture(quadratic, GL_TRUE);
}

/**
 * Here's Where We Do All The Drawing.
 */
void drawGLScene()
{
	// Clear The Screen And The Depth Buffer
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	// Reset The Current Modelview Matrix
	glLoadIdentity();

	// Translate Into/Out Of The Screen By z
	glTranslatef(0.0f, 0.0f, z);

	// Rotate On The X Axis By xrot
	glRotatef(xrot, 1.0f, 0.0f, 0.0f);
	// Rotate On The Y Axis By yrot
	glRotatef(yrot, 0.0f, 1.0f, 0.0f);

	// Select A Texture Based On filter
	glBindTexture(GL_TEXTURE_2D, textures[filter]);

	switch (obj)
	{
		case 0:
			glDrawCube();
			break;
		case 1:
			// Center The Cylinder
			glTranslatef(0.0f, 0.0f, -1.5f);
			// A Cylinder With A Radius Of 0.5 And A Height Of 2
			gluCylinder(quadratic, 1.0f, 1.0f, 3.0f, 32, 32);
			break;
		case 2:
			// Draw A Disc (CD Shape) With An Inner Radius Of 0.5, And An Outer Radius Of 2.  Plus A Lot Of Segments ;)
			gluDisk(quadratic, 0.5f, 1.5f, 32, 32);
			break;
		case 3:
			// Draw A Sphere With A Radius Of 1 And 16 Longitude And 16 Latitude Segments
			gluSphere(quadratic, 1.3f, 32, 32);
			break;
		case 4:
			// Center The Cone
			glTranslatef(0.0f, 0.0f, -1.5f);
			// A Cone With A Bottom Radius Of .5 And A Height Of 2
			gluCylinder(quadratic, 1.0f, 0.0f, 3.0f, 32, 32);
			break;
		case 5:
			part1 += p1;
			part2 += p2;

			// 360 Degrees
			if (part1 > 359)
			{
				p1 = 0;
				part1 = 0;
				p2 = 1;
				part2 = 0;
			}
			// 360 Degrees
			if (part2 > 359)
			{
				p1 = 1;
				p2 = 0;
			}
			// A Disk Like The One Before
			gluPartialDisk(quadratic, 0.5f, 1.5f, 32, 32, part1, part2 - part1);
			break;
	}

	// Add xspeed To xrot
	xrot += xspeed;
	// Add yspeed To yrot
	yrot += yspeed;
}

void glDrawCube()
{
	// Start Drawing Quads
	glBegin(GL_QUADS);
	{
		// Front Face
		// Normal Pointing Towards Viewer
		glNormal3f(0.0f, 0.0f, 1.0f);
		glTexCoord2f(0.0f, 0.0f);
		glVertex3f(-1.0f, -1.0f, 1.0f);
		glTexCoord2f(1.0f, 0.0f);
		glVertex3f(1.0f, -1.0f, 1.0f);
		glTexCoord2f(1.0f, 1.0f);
		glVertex3f(1.0f, 1.0f, 1.0f);
		glTexCoord2f(0.0f, 1.0f);
		glVertex3f(-1.0f, 1.0f, 1.0f);

		// Back Face
		// Normal Pointing Away From Viewer
		glNormal3f(0.0f, 0.0f, -1.0f);
		glTexCoord2f(1.0f, 0.0f);
		glVertex3f(-1.0f, -1.0f, -1.0f);
		glTexCoord2f(1.0f, 1.0f);
		glVertex3f(-1.0f, 1.0f, -1.0f);
		glTexCoord2f(0.0f, 1.0f);
		glVertex3f(1.0f, 1.0f, -1.0f);
		glTexCoord2f(0.0f, 0.0f);
		glVertex3f(1.0f, -1.0f, -1.0f);

		// Top Face
		// Normal Pointing Up
		glNormal3f(0.0f, 1.0f, 0.0f);
		glTexCoord2f(0.0f, 1.0f);
		glVertex3f(-1.0f, 1.0f, -1.0f);
		glTexCoord2f(0.0f, 0.0f);
		glVertex3f(-1.0f, 1.0f, 1.0f);
		glTexCoord2f(1.0f, 0.0f);
		glVertex3f(1.0f, 1.0f, 1.0f);
		glTexCoord2f(1.0f, 1.0f);
		glVertex3f(1.0f, 1.0f, -1.0f);

		// Bottom Face
		// Normal Pointing Down
		glNormal3f(0.0f, -1.0f, 0.0f);
		glTexCoord2f(1.0f, 1.0f);
		glVertex3f(-1.0f, -1.0f, -1.0f);
		glTexCoord2f(0.0f, 1.0f);
		glVertex3f(1.0f, -1.0f, -1.0f);
		glTexCoord2f(0.0f, 0.0f);
		glVertex3f(1.0f, -1.0f, 1.0f);
		glTexCoord2f(1.0f, 0.0f);
		glVertex3f(-1.0f, -1.0f, 1.0f);

		// Right face
		// Normal Pointing Right
		glNormal3f(1.0f, 0.0f, 0.0f);
		glTexCoord2f(1.0f, 0.0f);
		glVertex3f(1.0f, -1.0f, -1.0f);
		glTexCoord2f(1.0f, 1.0f);
		glVertex3f(1.0f, 1.0f, -1.0f);
		glTexCoord2f(0.0f, 1.0f);
		glVertex3f(1.0f, 1.0f, 1.0f);
		glTexCoord2f(0.0f, 0.0f);
		glVertex3f(1.0f, -1.0f, 1.0f);

		// Left Face
		// Normal Pointing Left
		glNormal3f(-1.0f, 0.0f, 0.0f);
		glTexCoord2f(0.0f, 0.0f);
		glVertex3f(-1.0f, -1.0f, -1.0f);
		glTexCoord2f(1.0f, 0.0f);
		glVertex3f(-1.0f, -1.0f, 1.0f);
		glTexCoord2f(1.0f, 1.0f);
		glVertex3f(-1.0f, 1.0f, 1.0f);
		glTexCoord2f(0.0f, 1.0f);
		glVertex3f(-1.0f, 1.0f, -1.0f);
	}
	glEnd(); // Done Drawing Quads
}

/**
 * Initializes and opens the SDL window.
 */
void createGLWindow(char[] title, int width, int height, int bits,
	bool fullScreen)
{
	// Set the OpenGL attributes
	SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 5);
	SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 6);
	SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 5);
	SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16);
	SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);

	// Set the window title
	SDL_WM_SetCaption(toStringz(title), null);

	/* Note the SDL_DOUBLEBUF flag is not required to enable double
	 * buffering when setting an OpenGL video mode.
	 * Double buffering is enabled or disabled using the
	 * SDL_GL_DOUBLEBUFFER attribute.
	 */
	int mode = SDL_OPENGL;
	if (fullScreen)
	{
		mode |= SDL_FULLSCREEN;
	}
	// Now open a SDL OpenGL window with the given parameters
	if (SDL_SetVideoMode(width, height, bits, mode) is null)
	{
		throw new Exception("Failed to open OpenGL window: " ~ getSDLError());
	}

	resizeGLScene(width, height);
}

/**
 * Get the SDL error as a D string.
 *
 * Returns: A D string containing the current SDL error.
 */
char[] getSDLError()
{
	return fromStringz(SDL_GetError());
}
